/*  XMMS - Cross-platform multimedia player
 *  Copyright (C) 1998-2001  Peter Alm, Mikael Alm, Olle Hallnas,
 *                           Thomas Nilsson and 4Front Technologies
 *  Copyright (C) 1999       Galex Yen
 *  Copyright (C) 1999-2001  Haavard Kvaalen
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "esdout.h"
#include "config.h"

static gint fd = 0;
static gpointer buffer;
static gboolean going = FALSE, paused = FALSE, prebuffer, remove_prebuffer;
static gint buffer_size, prebuffer_size, blk_size = 4096;
static gint rd_index = 0, wr_index = 0;
static gint output_time_offset = 0;
static guint64 written = 0, output_bytes = 0;
static gint bps, ebps;
static gint flush;
static gint format, channels, frequency, latency;
static esd_format_t esd_format;
static gint input_bps, input_format, input_frequency, input_channels;
static pthread_t buffer_thread;
static gboolean realtime = FALSE;
static void *(*esd_translate)(void *, gint);

static gint get_latency(void)
{
	int fd, amount = 0;

#ifndef HAVE_ESD_GET_LATENCY
	esd_server_info_t *info;
#endif

	fd = esd_open_sound(esd_cfg.hostname);

	if (fd == -1)
		return 0;

#ifdef HAVE_ESD_GET_LATENCY
	amount = esd_get_latency(fd);
#else
	info = esd_get_server_info(fd);
	if (info)
	{
		if (info->format & ESD_STEREO)
		{
			if (info->format & ESD_BITS16)
				amount = (44100 * (ESD_BUF_SIZE + 64)) / info->rate;
			else
				amount = (44100 * (ESD_BUF_SIZE + 128)) / info->rate;
		}
		else
		{
			if (info->format & ESD_BITS16)
				amount = (2 * 44100 * (ESD_BUF_SIZE + 128)) / info->rate;
			else
				amount = (2 * 44100 * (ESD_BUF_SIZE + 256)) / info->rate;
		}
		free(info);
	}
	amount += ESD_BUF_SIZE * 2;
#endif
	esd_close(fd);
	return amount;
}

static void *esd_stou8(void *data, gint length)
{
	int len = length;
	unsigned char *dat = (unsigned char *)data;
	while (len-- > 0)
		*dat++ ^= 0x80;
	return data;
}

static void *esd_utos16sw(void *data, gint length)
{
	int len = length;
	short *dat = data;
	while ( len > 0 ) {
		*dat = GUINT16_SWAP_LE_BE( *dat ) ^ 0x8000;
		dat++;
		len-=2;
	}
	return data;
}

static void *esd_utos16(void *data, gint length)
{
	int len = length;
	short *dat = data;
	while ( len > 0 ) {
		*dat ^= 0x8000;
		dat++;
		len-=2;
	}
	return data;
}

static void *esd_16sw(void *data, gint length)
{
	int len = length;
	short *dat = data;
	while ( len > 0 ) {
		*dat = GUINT16_SWAP_LE_BE( *dat );
		dat++;
		len-=2;
	}
	return data;
}

static void esdout_setup_format(AFormat fmt,gint rate, gint nch)
{
	gboolean swap_sign = FALSE;
	gboolean swap_16 = FALSE;

	format = fmt;
	frequency = rate;
	channels = nch;
	switch (fmt)
	{
		case FMT_S8:
			swap_sign = TRUE;
		case FMT_U8:
			esd_format = ESD_BITS8;
			break;
		case FMT_U16_LE:
		case FMT_U16_BE:
		case FMT_U16_NE:
			swap_sign = TRUE;
		case FMT_S16_LE:
		case FMT_S16_BE:
		case FMT_S16_NE:
			esd_format = ESD_BITS16;
			break;
	}

#ifdef WORDS_BIGENDIAN
	if (fmt == FMT_U16_LE || fmt == FMT_S16_LE)
#else
	if (fmt == FMT_U16_BE || fmt == FMT_S16_BE)
#endif
		swap_16 = TRUE;

	esd_translate = (void*(*)())NULL;
	if (esd_format == ESD_BITS8) {
		if (swap_sign == TRUE)
			esd_translate = esd_stou8;
	} else {
		if (swap_sign == TRUE) {
			if (swap_16 == TRUE)
				esd_translate = esd_utos16sw;
			else
				esd_translate = esd_utos16;
		} else {
			if (swap_16 == TRUE)
				esd_translate = esd_16sw;
		}
	}

	bps = rate * nch;
	if (esd_format == ESD_BITS16)
		bps *= 2;
	if(nch == 1)
		esd_format |= ESD_MONO;
	else
		esd_format |= ESD_STEREO;
	esd_format |= ESD_STREAM | ESD_PLAY;
	
	latency = ((get_latency() * frequency) / 44100) * channels;
	if (format != FMT_U8 && format != FMT_S8)
		latency *= 2;
}
	

gint esdout_get_written_time(void)
{
	if (!going)
		return 0;
	return (gint) ((written * 1000) / input_bps);
}

gint esdout_get_output_time(void)
{
	guint64 bytes;

	if (!fd || !going)
		return 0;

	bytes = output_bytes;
	if (!paused)
		bytes -= (bytes < latency ? bytes : latency);
	
	return output_time_offset + (bytes * 1000) / ebps;
}

gint esdout_used(void)
{
	if (realtime)
		return 0;
	else
	{
		if (wr_index >= rd_index)
			return wr_index - rd_index;
		return buffer_size - (rd_index - wr_index);
	}
}

gint esdout_playing(void)
{
	if (!going)
		return FALSE;
	if (!esdout_used())
		return FALSE;

	return TRUE;
}

gint esdout_free(void)
{
	if (!realtime)
	{
		if (remove_prebuffer && prebuffer)
		{
			prebuffer = FALSE;
			remove_prebuffer = FALSE;
		}
		if (prebuffer)
			remove_prebuffer = TRUE;

		if (rd_index > wr_index)
			return (rd_index - wr_index) - 1;
		return (buffer_size - (wr_index - rd_index)) - 1;
	}
	else
	{
		if (paused)
			return 0;
		else
			return 1000000;
	}
}

static void esdout_write_audio(gpointer data,gint length)
{
	AFormat new_format;
	gint new_frequency,new_channels;
	EffectPlugin *ep;
	
	new_format = input_format;
	new_frequency = input_frequency;
	new_channels = input_channels;
	
	ep = get_current_effect_plugin();
	if(effects_enabled() && ep && ep->query_format)
	{
		ep->query_format(&new_format,&new_frequency,&new_channels);
	}
	
	if(new_format != format || new_frequency != frequency || new_channels != channels)
	{
		output_time_offset += (output_bytes * 1000) / ebps;
		output_bytes = 0;
		esdout_setup_format(new_format, new_frequency, new_channels);
		frequency = new_frequency;
		channels = new_channels;
		esd_close(fd);
		esdout_set_audio_params();
	}
	if(effects_enabled() && ep && ep->mod_samples)
		length = ep->mod_samples(&data,length, input_format, input_frequency, input_channels);
        if (esd_translate)
                output_bytes += write(fd,esd_translate(data,length),length);
        else
                output_bytes += write(fd,data,length);
}


void esdout_write(gpointer ptr, gint length)
{
	gint cnt, off = 0;

	if (!realtime)
	{
		remove_prebuffer = FALSE;

		written += length;
		while (length > 0)
		{
			cnt = MIN(length, buffer_size - wr_index);
			memcpy((gchar *)buffer + wr_index, (gchar *)ptr + off, cnt);
			wr_index = (wr_index + cnt) % buffer_size;
			length -= cnt;
			off += cnt;

		}
	}
	else
	{
		if (paused)
			return;
		esdout_write_audio(ptr,length);
		written += length;
		
	}

}

void esdout_close(void)
{
	if (!going)
		return;

	going = 0;

	if (!realtime)
		pthread_join(buffer_thread, NULL);
	else
		esd_close(fd);

	wr_index = 0;
	rd_index = 0;
	g_free(esd_cfg.playername);
	esd_cfg.playername = NULL;
	esdout_reset_playerid();
}

void esdout_flush(gint time)
{
	if (!realtime)
	{
		flush = time;
		while (flush != -1)
			xmms_usleep(10000);
	}
	else
	{
		output_time_offset = time;
		written = (guint64)(time / 10) * (guint64)(input_bps / 100);
		output_bytes = 0;
	}
}

void esdout_pause(short p)
{
	paused = p;
}

void *esdout_loop(void *arg)
{
	gint length, cnt;
	

	while (going)
	{
		if (esdout_used() > prebuffer_size)
			prebuffer = FALSE;
		if (esdout_used() > 0 && !paused && !prebuffer)
		{
			length = MIN(blk_size, esdout_used());
			while (length > 0)
			{
				cnt = MIN(length,buffer_size-rd_index);
				esdout_write_audio((gchar *)buffer + rd_index, cnt);
				rd_index=(rd_index+cnt)%buffer_size;
				length-=cnt;				
			}
		}
		else
			xmms_usleep(10000);

		if (flush != -1)
		{
			output_time_offset = flush;
			written = (guint64)(flush / 10) * (guint64)(input_bps / 100);
			rd_index = wr_index = output_bytes = 0;
			flush = -1;
			prebuffer = TRUE;
		}

	}

	esd_close(fd);
	g_free(buffer);
	pthread_exit(NULL);
}

void esdout_set_audio_params(void)
{
	fd = esd_play_stream(esd_format, frequency,
			     esd_cfg.hostname, esd_cfg.playername);
	/* Set the stream's mixer */
	if (fd != -1)
		esdout_mixer_init();
	ebps = frequency * channels;
	if (format == FMT_U16_BE || format == FMT_U16_LE ||
	    format == FMT_S16_BE || format == FMT_S16_LE ||
	    format == FMT_S16_NE || format == FMT_U16_NE)
		ebps *= 2;
}

int esdout_open(AFormat fmt, int rate, int nch)
{
	static unsigned int playercnt = 0;

	esdout_setup_format(fmt,rate,nch);
	
	input_format = format;
	input_channels = channels;
	input_frequency = frequency;
	input_bps = bps;

	realtime = xmms_check_realtime_priority();
	
	if (!realtime)
	{
		buffer_size = (esd_cfg.buffer_size * input_bps) / 1000;
		if (buffer_size < 8192)
			buffer_size = 8192;
		prebuffer_size = (buffer_size * esd_cfg.prebuffer) / 100;
		if (buffer_size - prebuffer_size < 4096)
			prebuffer_size = buffer_size - 4096;

		buffer = g_malloc0(buffer_size);
	}
	flush = -1;
	prebuffer = 1;
	wr_index = rd_index = output_time_offset = written = output_bytes = 0;
	paused = FALSE;
	remove_prebuffer = FALSE;

	esd_cfg.playername = g_strdup_printf("xmms - plugin (%d-%u)",
					     getpid(), playercnt++);

	if (esd_cfg.hostname)
		g_free(esd_cfg.hostname);
	if (esd_cfg.use_remote)
		esd_cfg.hostname = g_strdup_printf("%s:%d", esd_cfg.server, esd_cfg.port);
	else
		esd_cfg.hostname = NULL;
	
	esdout_set_audio_params();
	if (fd == -1)
	{
		g_free(esd_cfg.playername);
		esd_cfg.playername = NULL;
		g_free(buffer);
		return 0;
	}
	going = 1;

	if (!realtime)
		pthread_create(&buffer_thread, NULL, esdout_loop, NULL);
	return 1;
}
